/*
 * MultiSelect v0.9.12
 * Copyright (c) 2012 Louis Cuny
 *
 * This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the Do What The Fuck You Want
 * To Public License, Version 2, as published by Sam Hocevar. See
 * http://sam.zoy.org/wtfpl/COPYING for more details.
 */

!function($) {

	"use strict";

	/*
	 * MULTISELECT CLASS DEFINITION ======================
	 */

	var MultiSelect = function(element, options) {
		this.options = options;
		this.$element = $(element);
		this.$container = $('<div/>', {
			'class' : "ms-container"
		});
		this.$selectableContainer = $('<div/>', {
			'class' : 'ms-selectable'
		});
		this.$selectionContainer = $('<div/>', {
			'class' : 'ms-selection'
		});
		this.$selectableUl = $('<ul/>', {
			'class' : "ms-list",
			'tabindex' : '-1'
		});
		this.$selectionUl = $('<ul/>', {
			'class' : "ms-list",
			'tabindex' : '-1'
		});
		this.scrollTo = 0;
		this.elemsSelector = 'li:visible:not(.ms-optgroup-label,.ms-optgroup-container,.'
				+ options.disabledClass + ')';
	};

	MultiSelect.prototype = {
		constructor : MultiSelect,

		init : function() {

			var that = this, ms = this.$element;

			if (ms.next('.ms-container').length === 0) {

				ms.css({
					position : 'absolute',
					left : '-9999px'
				});
				ms.attr('id', ms.attr('id') ? ms.attr('id') : Math.ceil(Math
						.random() * 1000)
						+ 'multiselect');
				this.$container.attr('id', 'ms-' + ms.attr('id'));
				this.$container.addClass(that.options.cssClass);
				ms.find('option').each(function() {
					that.generateLisFromOption(this);
				});

				this.$selectionUl.find('.ms-optgroup-label').hide();
				if (that.options.selectableHeader) {
					that.$selectableContainer
							.append(that.options.selectableHeader);
				}
				that.$selectableContainer.append(that.$selectableUl);
				if (that.options.selectableFooter) {
					that.$selectableContainer
							.append(that.options.selectableFooter);
				}

				if (that.options.selectionHeader) {
					that.$selectionContainer
							.append(that.options.selectionHeader);
				}
				that.$selectionContainer.append(that.$selectionUl);
				if (that.options.selectionFooter) {
					that.$selectionContainer
							.append(that.options.selectionFooter);
				}

				that.$container.append(that.$selectableContainer);
				that.$container.append(that.$selectionContainer);
				ms.after(that.$container);

				that.activeMouse(that.$selectableUl);
				that.activeKeyboard(that.$selectableUl);

				var action = that.options.dblClick ? 'dblclick' : 'click';

				that.$selectableUl.on(action, '.ms-elem-selectable',
						function() {
							that.select($(this).data('ms-value'));
						});
				that.$selectionUl.on(action, '.ms-elem-selection', function() {
					that.deselect($(this).data('ms-value'));
				});

				that.activeMouse(that.$selectionUl);
				that.activeKeyboard(that.$selectionUl);

				ms.on('focus', function() {
					that.$selectableUl.focus();
				});
			}

			var selectedValues = ms.find('option:selected').map(function() {
				return $(this).val();
			}).get();
			that.select(selectedValues, 'init');

			if (typeof that.options.afterInit === 'function') {
				that.options.afterInit.call(this, this.$container);
			}
		},

		'generateLisFromOption' : function(option, index, $container) {
			var that = this, ms = that.$element, attributes = "", $option = $(option);

			for (var cpt = 0; cpt < option.attributes.length; cpt++) {
				var attr = option.attributes[cpt];

				if (attr.name !== 'value' && attr.name !== 'disabled') {
					attributes += attr.name + '="' + attr.value + '" ';
				}
			}
			var selectableLi = $('<li ' + attributes + '><span>'
					+ that.escapeHTML($option.text()) + '</span></li>'), selectedLi = selectableLi
					.clone(), value = $option.val(), elementId = that
					.sanitize(value);

			selectableLi.data('ms-value', value).addClass('ms-elem-selectable')
					.attr('id', elementId + '-selectable');

			selectedLi.data('ms-value', value).addClass('ms-elem-selection')
					.attr('id', elementId + '-selection').hide();

			if ($option.prop('disabled') || ms.prop('disabled')) {
				selectedLi.addClass(that.options.disabledClass);
				selectableLi.addClass(that.options.disabledClass);
			}

			var $optgroup = $option.parent('optgroup');

			if ($optgroup.length > 0) {
				var optgroupLabel = $optgroup.attr('label'), optgroupId = that
						.sanitize(optgroupLabel), $selectableOptgroup = that.$selectableUl
						.find('#optgroup-selectable-' + optgroupId), $selectionOptgroup = that.$selectionUl
						.find('#optgroup-selection-' + optgroupId);

				if ($selectableOptgroup.length === 0) {
					var optgroupContainerTpl = '<li class="ms-optgroup-container"></li>', optgroupTpl = '<ul class="ms-optgroup"><li class="ms-optgroup-label"><span>'
							+ optgroupLabel + '</span></li></ul>';

					$selectableOptgroup = $(optgroupContainerTpl);
					$selectionOptgroup = $(optgroupContainerTpl);
					$selectableOptgroup.attr('id', 'optgroup-selectable-'
							+ optgroupId);
					$selectionOptgroup.attr('id', 'optgroup-selection-'
							+ optgroupId);
					$selectableOptgroup.append($(optgroupTpl));
					$selectionOptgroup.append($(optgroupTpl));
					if (that.options.selectableOptgroup) {
						$selectableOptgroup.find('.ms-optgroup-label').on(
								'click',
								function() {
									var values = $optgroup.children(
											':not(:selected, :disabled)').map(
											function() {
												return $(this).val();
											}).get();
									that.select(values);
								});
						$selectionOptgroup.find('.ms-optgroup-label').on(
								'click',
								function() {
									var values = $optgroup.children(
											':selected:not(:disabled)').map(
											function() {
												return $(this).val();
											}).get();
									that.deselect(values);
								});
					}
					that.$selectableUl.append($selectableOptgroup);
					that.$selectionUl.append($selectionOptgroup);
				}
				index = index === undefined ? $selectableOptgroup.find('ul')
						.children().length : index + 1;
				selectableLi.insertAt(index, $selectableOptgroup.children());
				selectedLi.insertAt(index, $selectionOptgroup.children());
			} else {
				index = index === undefined ? that.$selectableUl.children().length
						: index;

				selectableLi.insertAt(index, that.$selectableUl);
				selectedLi.insertAt(index, that.$selectionUl);
			}
		},

		'addOption' : function(options) {
			var that = this;

			if (options.value !== undefined && options.value !== null) {
				options = [ options ];
			}
			$
					.each(
							options,
							function(index, option) {
								if (option.value !== undefined
										&& option.value !== null
										&& that.$element.find("option[value='"
												+ option.value + "']").length === 0) {
									var $option = $('<option value="'
											+ option.value + '">' + option.text
											+ '</option>'), $container = option.nested === undefined ? that.$element
											: $("optgroup[label='"
													+ option.nested + "']"), index = parseInt((typeof option.index === 'undefined' ? $container
											.children().length
											: option.index));

									if (option.optionClass) {
										$option.addClass(option.optionClass);
									}

									if (option.disabled) {
										$option.prop('disabled', true);
									}

									$option.insertAt(index, $container);
									that.generateLisFromOption($option.get(0),
											index, option.nested);
								}
							});
		},

		'escapeHTML' : function(text) {
			return $("<div>").text(text).html();
		},

		'activeKeyboard' : function($list) {
			var that = this;

			$list.on('focus', function() {
				$(this).addClass('ms-focus');
			}).on('blur', function() {
				$(this).removeClass('ms-focus');
			}).on(
					'keydown',
					function(e) {
						switch (e.which) {
						case 40:
						case 38:
							e.preventDefault();
							e.stopPropagation();
							that.moveHighlight($(this), (e.which === 38) ? -1
									: 1);
							return;
						case 37:
						case 39:
							e.preventDefault();
							e.stopPropagation();
							that.switchList($list);
							return;
						case 9:
							if (that.$element.is('[tabindex]')) {
								e.preventDefault();
								var tabindex = parseInt(that.$element
										.attr('tabindex'), 10);
								tabindex = (e.shiftKey) ? tabindex - 1
										: tabindex + 1;
								$('[tabindex="' + (tabindex) + '"]').focus();
								return;
							} else {
								if (e.shiftKey) {
									that.$element.trigger('focus');
								}
							}
						}
						if ($.inArray(e.which, that.options.keySelect) > -1) {
							e.preventDefault();
							e.stopPropagation();
							that.selectHighlighted($list);
							return;
						}
					});
		},

		'moveHighlight' : function($list, direction) {
			var $elems = $list.find(this.elemsSelector), $currElem = $elems
					.filter('.ms-hover'), $nextElem = null, elemHeight = $elems
					.first().outerHeight(), containerHeight = $list.height(), containerSelector = '#'
					+ this.$container.prop('id');

			$elems.removeClass('ms-hover');
			if (direction === 1) { // DOWN

				$nextElem = $currElem.nextAll(this.elemsSelector).first();
				if ($nextElem.length === 0) {
					var $optgroupUl = $currElem.parent();

					if ($optgroupUl.hasClass('ms-optgroup')) {
						var $optgroupLi = $optgroupUl.parent(), $nextOptgroupLi = $optgroupLi
								.next(':visible');

						if ($nextOptgroupLi.length > 0) {
							$nextElem = $nextOptgroupLi
									.find(this.elemsSelector).first();
						} else {
							$nextElem = $elems.first();
						}
					} else {
						$nextElem = $elems.first();
					}
				}
			} else if (direction === -1) { // UP

				$nextElem = $currElem.prevAll(this.elemsSelector).first();
				if ($nextElem.length === 0) {
					var $optgroupUl = $currElem.parent();

					if ($optgroupUl.hasClass('ms-optgroup')) {
						var $optgroupLi = $optgroupUl.parent(), $prevOptgroupLi = $optgroupLi
								.prev(':visible');

						if ($prevOptgroupLi.length > 0) {
							$nextElem = $prevOptgroupLi
									.find(this.elemsSelector).last();
						} else {
							$nextElem = $elems.last();
						}
					} else {
						$nextElem = $elems.last();
					}
				}
			}
			if ($nextElem.length > 0) {
				$nextElem.addClass('ms-hover');
				var scrollTo = $list.scrollTop() + $nextElem.position().top
						- containerHeight / 2 + elemHeight / 2;

				$list.scrollTop(scrollTo);
			}
		},

		'selectHighlighted' : function($list) {
			var $elems = $list.find(this.elemsSelector), $highlightedElem = $elems
					.filter('.ms-hover').first();

			if ($highlightedElem.length > 0) {
				if ($list.parent().hasClass('ms-selectable')) {
					this.select($highlightedElem.data('ms-value'));
				} else {
					this.deselect($highlightedElem.data('ms-value'));
				}
				$elems.removeClass('ms-hover');
			}
		},

		'switchList' : function($list) {
			$list.blur();
			this.$container.find(this.elemsSelector).removeClass('ms-hover');
			if ($list.parent().hasClass('ms-selectable')) {
				this.$selectionUl.focus();
			} else {
				this.$selectableUl.focus();
			}
		},

		'activeMouse' : function($list) {
			var that = this;

			this.$container.on('mouseenter', that.elemsSelector, function() {
				$(this).parents('.ms-container').find(that.elemsSelector)
						.removeClass('ms-hover');
				$(this).addClass('ms-hover');
			});

			this.$container.on('mouseleave', that.elemsSelector, function() {
				$(this).parents('.ms-container').find(that.elemsSelector)
						.removeClass('ms-hover');
			});
		},

		'refresh' : function() {
			this.destroy();
			this.$element.multiSelect(this.options);
		},

		'destroy' : function() {
			$("#ms-" + this.$element.attr("id")).remove();
			this.$element.off('focus');
			this.$element.css('position', '').css('left', '');
			this.$element.removeData('multiselect');
		},

		'select' : function(value, method) {
			if (typeof value === 'string') {
				value = [ value ];
			}

			var that = this, ms = this.$element, msIds = $.map(value, function(
					val) {
				return (that.sanitize(val));
			}), selectables = this.$selectableUl.find(
					'#' + msIds.join('-selectable, #') + '-selectable').filter(
					':not(.' + that.options.disabledClass + ')'), selections = this.$selectionUl
					.find('#' + msIds.join('-selection, #') + '-selection')
					.filter(':not(.' + that.options.disabledClass + ')'), options = ms
					.find('option:not(:disabled)').filter(function() {
						return ($.inArray(this.value, value) > -1);
					});

			if (method === 'init') {
				selectables = this.$selectableUl.find('#'
						+ msIds.join('-selectable, #') + '-selectable'),
						selections = this.$selectionUl.find('#'
								+ msIds.join('-selection, #') + '-selection');
			}

			if (selectables.length > 0) {
				selectables.addClass('ms-selected').hide();
				selections.addClass('ms-selected').show();

				options.prop('selected', true);

				that.$container.find(that.elemsSelector)
						.removeClass('ms-hover');

				var selectableOptgroups = that.$selectableUl
						.children('.ms-optgroup-container');
				if (selectableOptgroups.length > 0) {
					selectableOptgroups
							.each(function() {
								var selectablesLi = $(this).find(
										'.ms-elem-selectable');
								if (selectablesLi.length === selectablesLi
										.filter('.ms-selected').length) {
									$(this).find('.ms-optgroup-label').hide();
								}
							});

					var selectionOptgroups = that.$selectionUl
							.children('.ms-optgroup-container');
					selectionOptgroups.each(function() {
						var selectionsLi = $(this).find('.ms-elem-selection');
						if (selectionsLi.filter('.ms-selected').length > 0) {
							$(this).find('.ms-optgroup-label').show();
						}
					});
				} else {
					if (that.options.keepOrder && method !== 'init') {
						var selectionLiLast = that.$selectionUl
								.find('.ms-selected');
						if ((selectionLiLast.length > 1)
								&& (selectionLiLast.last().get(0) != selections
										.get(0))) {
							selections.insertAfter(selectionLiLast.last());
						}
					}
				}
				if (method !== 'init') {
					ms.trigger('change');
					if (typeof that.options.afterSelect === 'function') {
						that.options.afterSelect.call(this, value);
					}
				}
			}
		},

		'deselect' : function(value) {
			if (typeof value === 'string') {
				value = [ value ];
			}

			var that = this, ms = this.$element, msIds = $.map(value, function(
					val) {
				return (that.sanitize(val));
			}), selectables = this.$selectableUl.find('#'
					+ msIds.join('-selectable, #') + '-selectable'), selections = this.$selectionUl
					.find('#' + msIds.join('-selection, #') + '-selection')
					.filter('.ms-selected').filter(
							':not(.' + that.options.disabledClass + ')'), options = ms
					.find('option').filter(function() {
						return ($.inArray(this.value, value) > -1);
					});

			if (selections.length > 0) {
				selectables.removeClass('ms-selected').show();
				selections.removeClass('ms-selected').hide();
				options.prop('selected', false);

				that.$container.find(that.elemsSelector)
						.removeClass('ms-hover');

				var selectableOptgroups = that.$selectableUl
						.children('.ms-optgroup-container');
				if (selectableOptgroups.length > 0) {
					selectableOptgroups
							.each(function() {
								var selectablesLi = $(this).find(
										'.ms-elem-selectable');
								if (selectablesLi.filter(':not(.ms-selected)').length > 0) {
									$(this).find('.ms-optgroup-label').show();
								}
							});

					var selectionOptgroups = that.$selectionUl
							.children('.ms-optgroup-container');
					selectionOptgroups.each(function() {
						var selectionsLi = $(this).find('.ms-elem-selection');
						if (selectionsLi.filter('.ms-selected').length === 0) {
							$(this).find('.ms-optgroup-label').hide();
						}
					});
				}
				ms.trigger('change');
				if (typeof that.options.afterDeselect === 'function') {
					that.options.afterDeselect.call(this, value);
				}
			}
		},

		'select_all' : function() {
			var ms = this.$element, values = ms.val();

			ms.find('option:not(":disabled")').prop('selected', true);
			this.$selectableUl.find('.ms-elem-selectable').filter(
					':not(.' + this.options.disabledClass + ')').addClass(
					'ms-selected').hide();
			this.$selectionUl.find('.ms-optgroup-label').show();
			this.$selectableUl.find('.ms-optgroup-label').hide();
			this.$selectionUl.find('.ms-elem-selection').filter(
					':not(.' + this.options.disabledClass + ')').addClass(
					'ms-selected').show();
			this.$selectionUl.focus();
			ms.trigger('change');
			if (typeof this.options.afterSelect === 'function') {
				var selectedValues = $.grep(ms.val(), function(item) {
					return $.inArray(item, values) < 0;
				});
				this.options.afterSelect.call(this, selectedValues);
			}
		},

		'deselect_all' : function() {
			var ms = this.$element, values = ms.val();

			ms.find('option').prop('selected', false);
			this.$selectableUl.find('.ms-elem-selectable').removeClass(
					'ms-selected').show();
			this.$selectionUl.find('.ms-optgroup-label').hide();
			this.$selectableUl.find('.ms-optgroup-label').show();
			this.$selectionUl.find('.ms-elem-selection').removeClass(
					'ms-selected').hide();
			this.$selectableUl.focus();
			ms.trigger('change');
			if (typeof this.options.afterDeselect === 'function') {
				this.options.afterDeselect.call(this, values);
			}
		},

		sanitize : function(value) {
			var hash = 0, i, character;
			if (value.length == 0)
				return hash;
			var ls = 0;
			for (i = 0, ls = value.length; i < ls; i++) {
				character = value.charCodeAt(i);
				hash = ((hash << 5) - hash) + character;
				hash |= 0; // Convert to 32bit integer
			}
			return hash;
		}
	};

	/*
	 * MULTISELECT PLUGIN DEFINITION =======================
	 */

	$.fn.multiSelect = function() {
		var option = arguments[0], args = arguments;

		return this.each(function() {
			var $this = $(this), data = $this.data('multiselect'), options = $
					.extend({}, $.fn.multiSelect.defaults, $this.data(),
							typeof option === 'object' && option);

			if (!data) {
				$this.data('multiselect',
						(data = new MultiSelect(this, options)));
			}

			if (typeof option === 'string') {
				data[option](args[1]);
			} else {
				data.init();
			}
		});
	};

	$.fn.multiSelect.defaults = {
		keySelect : [ 32 ],
		selectableOptgroup : false,
		disabledClass : 'disabled',
		dblClick : false,
		keepOrder : false,
		cssClass : ''
	};

	$.fn.multiSelect.Constructor = MultiSelect;

	$.fn.insertAt = function(index, $parent) {
		return this.each(function() {
			if (index === 0) {
				$parent.prepend(this);
			} else {
				$parent.children().eq(index - 1).after(this);
			}
		});
	};

}(window.jQuery);
